﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using TracedReality.RayTracing;
using TracedReality.RayTracing.PhotonMapping;
using QiHe.Yaml.Grammar;

namespace TracedReality.Primitive
{
    /// <summary>
    /// A Light that is a planar surface and may cause umbras.
    /// </summary>
    public class AreaLight : Light
    {
        /// <summary>
        /// Corner of the plane.
        /// </summary>
        private Vector corner;

        /// <summary>
        /// First direction of the plane.
        /// </summary>
        private Vector d1;

        /// <summary>
        /// Second direction of the plane.
        /// </summary>
        private Vector d2;

        /// <summary>
        /// Normal of the plane.
        /// </summary>
        private Vector normal;

        /// <summary>
        /// Center of the plane.
        /// </summary>
        public Vector center;

        /// <summary>
        /// Create a new AreaLight with the given plane and color.
        /// </summary>
        /// <param name="c">Corner of the plane.</param>
        /// <param name="a">First direction of the plane.</param>
        /// <param name="b">Second direction of the plane.</param>
        /// <param name="col">Color of the AreaLight.</param>
        public AreaLight(Vector c, Vector a, Vector b, Vector col)
        {
            corner = c;
            d1 = a;
            d2 = b;
            color = col;

            normal = d1.cross(d2);
            normal.normalize();

            Vector w = corner;
            Vector x = corner + d1;
            Vector y = corner + d1 + d2;
            Vector z = corner + d2;

            center = 0.25 * (w + x + y + z);
        }

        /// <summary>
        /// Get a random point on this AreaLight.
        /// </summary>
        /// <param name="r">Random to use.</param>
        /// <returns>A random point on this AreaLight.</returns>
        private Vector randomPoint(Random r)
        {
            Vector a = corner;
            Vector b = corner + d1;
            Vector c = corner + d1 + d2;
            Vector d = corner + d2;
            double x = r.NextDouble();
            double y = r.NextDouble();

            return ((1 - x) * (1 - y) * a) + (x * (1 - y) * b) + (x * y * c) + ((1 - x) * y * d);
        }

        /// <summary>
        /// Determine how much light from this AreaLight is received at the given Hit.
        /// Uses sampling if enabled in the Settings.
        /// </summary>
        /// <param name="ray">The Ray that is being traced.</param>
        /// <param name="hit">The Hit containing the intersection info.</param>
        /// <param name="r">Random for sampling.</param>
        /// <returns>The contribution of this AreaLight.</returns>
        public override Vector getContribution(Ray ray, Hit hit, Random r)
        {
            Vector contrib = new Vector();
            Vector point = ray.pointAtParameter(hit.t);
            Vector lightDir = center - point;
            lightDir.normalize();

            if (Settings.numShadowSamples == 0)
            {
                return hit.material.shade(ray, hit, lightDir, color);
            }

            int hits = 0;
            int nbest = 1;
            while ((nbest * nbest) < Settings.numShadowSamples) { nbest++; }
            Vector[,] strata = Utilities.stratify(corner, d1, d2, nbest, (Settings.numShadowSamples != 1), r);
            Vector[] fltStrata = Utilities.flatten(strata);
            Vector[] samples = Utilities.sample(fltStrata, Settings.numShadowSamples, r);
            for (int s = 0; s < Settings.numShadowSamples; s++)
            {
                Vector lPoint = samples[s];

                Vector lpDir = lPoint - point;
                double dst = lpDir.length;
                lpDir.normalize();

                Ray shadow = new Ray(point, lpDir);
                if (RayTracer.intersectsAnythingBefore(shadow, dst)) { continue; }
                hits++;
            }

            return hit.material.shade(ray, hit, lightDir, color) * (hits / (double)Settings.numShadowSamples);
        }

        /// <summary>
        /// Get the area of the plane representing this AreaLight.
        /// </summary>
        /// <returns>The area of the plane.</returns>
        private double area()
        {
            return d1.length * d2.length;
        }

        /// <summary>
        /// Generates a Photon of the correct power and direction for photon mapping.
        /// </summary>
        /// <param name="num">The number of Photons that will be shot from this AreaLight.</param>
        /// <param name="r">Random for sampling.</param>
        /// <returns>A Photon with correct power and direction.</returns>
        public override Photon generatePhoton(int num, Random r)
        {
            Vector dir = Vector.RandomDiffuseVector(normal, r);
            Vector pos = corner + (r.NextDouble() * d1) + (r.NextDouble() * d2);
            return new Photon(pos, color / (area() * area()), dir);
        }

        public static AreaLight Parse(MappingEntry entry)
        {
            if (entry == null) { throw new ArgumentNullException("entry"); }

            Mapping map = (Mapping)entry.Value;
            MappingEntry found = map.Find("color");
            if (found == null) { throw new ArgumentException("No color entry"); }
            Vector color = Vector.FromSequence((Sequence)found.Value);
            found = map.Find("corner");
            if (found == null) { throw new ArgumentException("No corner entry"); }
            Vector corner = Vector.FromSequence((Sequence)found.Value);
            found = map.Find("dir1");
            if (found == null) { throw new ArgumentException("No dir1 entry"); }
            Vector dir1 = Vector.FromSequence((Sequence)found.Value);
            found = map.Find("dir2");
            if (found == null) { throw new ArgumentException("No dir2 entry"); }
            Vector dir2 = Vector.FromSequence((Sequence)found.Value);

            return new AreaLight(corner, dir1, dir2, color);
        }

        /// <summary>
        /// Gets a string representation of this AreaLight.
        /// </summary>
        /// <returns>A string representing this AreaLight.</returns>
        public override string ToString()
        {
            return "AreaLight: { corner: " + corner + ", dir1: " + d1 + ", dir2: " + d2 + ", color: " + color + " }";
        }
    }
}
